__author__ = 'Brandon Corfman'
import random
import term
from collections import namedtuple
from agent import Agent
from actions import RepairOne
from path import Path, Goal
from search import dfs
from problem import FindLeavesProblem
from utils import manhattan_distance, argmin, argmax_random_tie, first

class ComputerAgent(Agent):
    def __init__(self, hand, num_players):
        Agent.__init__(self, hand, num_players)
        self.goal_knowledge = [Goal('NSEW', 'unknown') for _ in range(3)]
        self.player_roles = [None for _ in num_players]

    def receive(self, msg):
        # TODO: write this method to process useful information about actions that
        # TODO: have taken place by other Agents.
        action, rest = msg.split(None, 1)
        if action == 'Peek':
            name, pos_idx = rest.split()
            pos = int(pos_idx)
        elif action == 'BrokenTool':
            from_agent, to_agent, state = rest.split()
        elif action == 'RepairOne':
            from_agent, to_agent, state = rest.split()
        elif action == 'RepairAny':
            from_agent, to_agent, state = rest.split()
        elif action == 'RockFall':
            agent, row_idx, col_idx = rest.split()
            row, col = int(row_idx), int(col_idx)
        elif action == 'Discard':
            agent, card_idx = rest.split()
            idx = int(card_idx)
        elif action == 'SetPath':
            agent, row_idx, col_idx, path_char = rest.split()
            row, col = int(row_idx), int(col_idx)
            
    def add_card_to_hand(self, card):
        self.hand.append(card)

    def play_card(self, board, others):
        """
        Let the AI pick and play a card from those in its hand.
        """
        selection = argmax_random_tie([card.precondition(self, board, others)
                                       for card in self.hand], first)
        action, index, card = selection
        card.execute(self, board, others)
        if action == 'p' or action == 'd': # play or discard
            self.hand.pop(index)
        return action

    def is_item_known(self, item):
        found = False
        for g in self.goal_knowledge:
            if g.state == item:
                found = True
                break
        return g if found else None

    def evaluate_leaves(self, best, goal):
        # if closest tunnel is directly in line with the goal, then try to discard
        # any paths that lead away from it or that don't connect with the current
        # tunnel.
        rank = 0.0
        if best.row == goal.row and (card_str == chr(0x2502) or
                                     card_str == chr(0x251C) or
                                     card_str == chr(0x2524) or
                                     card_str == chr(0x2518) or
                                     card_str == chr(0x250C) or
                                     card_str == chr(0x2514)):
            rank = 0.4
        # if closest tunnel has a larger row index than the goal, then try to discard
        # any paths that lead the wrong direction (SW or NE).
        elif best.row > goal.row and (card_str == chr(0x2518) or
                                      card_str == chr(0x250C)):
            rank = 0.4
        # discard SE or NW paths if best row is less than goal row
        elif best.row < goal.row and (card_str == chr(0x2510) or
                                      card_str == chr(0x2514)):
            rank = 0.4
        return rank

    def decide_discard(self, board, card):
        rank = 0.1 # start with slight desire to discard
        other_cards = [self.hand(i) for i in range(len(self.hand)) if i != card.idx]
        card_str = str(card)

        # if card is a Path, then discard if it's one that doesn't help us get
        # potentially closer to the gold.
        if isinstance(card, Path):
            goal = self.is_item_known('gold')
            if goal:
                start = board.path[4][1]
                leaves = [path for path, _, _ in dfs(FindLeavesProblem(start, board))
                          if path.enter == path.exit]
                calc_dist = lambda p: manhattan_distance(goal.row, goal.col, p.row, p.col)
                best = argmin(leaves, calc_dist)
                rank = self.evaluate_leaves(best, calc_dist)
            else: # only unknowns or stones
                # look for stones
                stone = self.is_item_known('stone')
                if stone:
                    # anything that's not a stone has potential to be gold
                    GridLoc = namedtuple('GridLoc', 'row, col')
                    goal_locations = [GridLoc(row=2, col=stone.col),
                                      GridLoc(row=4, col=stone.col),
                                      GridLoc(row=6, col=stone.col)]
                    goal_locations.remove(GridLoc(row=stone.row, col=stone.col))
                    start = board.path[4][1]
                    leaves = [path for path, _, _ in dfs(FindLeavesProblem(start, board))
                              if path.enter == path.exit]
                    for g in goal_locations:
                        calc_dist = lambda p: manhattan_distance(g.row, g.col, p.row, p.col)
                        best = argmin(leaves, calc_dist)
                        rank = max(rank, self.evaluate_leaves(best, calc_dist))
                else:
                    # all goals unknown -- simply pass and the last check will rank
                    # any repeated Path cards in the hand with higher values.
                    pass

        # if card is a Peek and we already know the location of the gold, then
        # the Peek is useless.
        if card_str == 'Peek' and any([g for g in self.goal_knowledge if g.state == 'gold']):
            rank = 1.0

        # If card is a RockFall and there is a dead end preventing us from reaching the
        # Goal, then discarding the RockFall could be a bad idea based on how far the goal
        # is from the dead end. If there are no dead ends, keeping the RockFall might be
        # a good idea but it's not critical.
        if card_str == 'RockFall':
            found_goal = False
            for g in self.goal_knowledge:
                if g.state == 'gold':
                    found_goal = True
                    break
            if found_goal:
                start = board.path[4][1]
                dead_ends = [path for path, _, _ in dfs(FindLeavesProblem(start, board))
                             if path.enter != path.exit]
                calc_dist = lambda p: manhattan_distance(g.row, g.col, p.row, p.col)
                min_dist = argmin(dead_ends, calc_dist)
                # closer to goal, then less desire to discard
                rank = min_dist / (board.board_size * 2.0)
            else:
                rank = 0.4

        # if card is a RepairOne or RepairAll, and we have a broken tool that can
        # be repaired, then discarding is a bad idea. If there are no broken tools,
        # keeping the Repair card at the ready is still a pretty good idea.
        if card_str == 'RepairOne':
            if card.state not in self.states:
                rank = 0.3
        elif card_str == 'RepairAny':
            if card.state1 in self.states and card.state2 in self.states:
                rank = 0.05
            elif card.state1 not in self.states and card.state2 not in self.states:
                rank = 0.3

        # double rank value for each duplicate card in hand
        for c in other_cards:
            if isinstance(c, Path) and str(c) == str(card):
                rank *= 2.0

        # max out at 0.7 since discarding is okay, but we don't want to perform
        # a discard if there's a more direct action that gets us closer to the Goal.
        return min(rank, 0.7)

    def decide_path_card(self, board, card):
        return rank, row, col

    def decide_rockfall(self, board, card):
        return 0.0
        #if isinstance(self.parent.dwarf, GoldDigger):
            #for row, col in self.possible_gold_locations:
            #    best_dist = 999999
            #    best_row, best_col = None, None
            #    do a depth-first search (starting from Ladder)
            #    for r,c in search_fringe_ends:
            #        if isinstance(board.grid[r][c], DeadEnd):
            #            dist = euclidean_distance((row, col), (r, c))
            #            if dist < best_dist:
            #                best_row, best_col = r, c
            #                best_dist = dist
            #        if best_dist == 999999:
            #            return 0.0
            #        else:
            #            return 1.0 / best_dist
            #else:
            #    return 0.0
        #return rank, row, col

    def decide_peek(self, board, card):
        num_known_goals = 0
        idx_unknown_goals = []
        for i, state in self.goal_knowledge:
            if state == 'gold':
                return 0.0
            elif state != 'unknown':
                num_known_goals += 1
                idx_unknown_goals.append(i)
        return 1.0 - num_known_goals / 3.0, random.choice(idx_unknown_goals)

    def decide_repair_any(self, board, card, others):
        if card.state1 not in self.states and card.state2 not in self.states:
            return 0.0, None

        if card.state1 in self.states and card.state2 not in self.states:
            if RepairOne(card.state1) in self.hand:
                return 0.0, None
            else:
                return 1.0, card.state1

        if card.state2 in self.states and card.state1 not in self.states:
            if RepairOne(card.state2) in self.hand:
                return 0.0, None
            else:
                return 1.0, card.state2

        if card.state1 in self.states and card.state2 in self.states:
            if RepairOne(card.state1) in self.hand and RepairOne(card.state2) not in self.hand:
                return 1.0, card.state2
            elif RepairOne(card.state1) not in self.hand and RepairOne(card.state2) in self.hand:
                return 1.0, card.state1
            elif RepairOne(card.state1) not in self.hand and RepairOne(card.state2) not in self.hand:
                return 1.0, random.choice([card.state1, card.state2])

        return 0.0, None

    def decide_broken_tool(self, board, state, others):
        return 0.0, None
        #return rank, others[n-1]
